var DB = ((mongoose, db_name) => {

	var utils = require('./utils');

	//==============================
	// roles definition

	var ROLE = {
		admin: '管理员',
		oaa: '教务处',
		teacher: '教师',
		student: '学生'
	};

	//==============================

	//==============================
	// schemas definition

	var Schema = mongoose.Schema;

	//------------------------------
	// {counters}

	// simulate auto-incrementing field
	var schema_counters = new Schema({
		name: String,
		seq: Number
	});

	//------------------------------
	// {users}

	// 'seq' should auto-increment whenever
	// create, change infomation or revoke
	var schema_users = new Schema({
		id: { type: Number, unique: true },
		seq: Number,
		username: String,
		credential: String,
		role: String,
		name: String,
		telephone: Number,
	}, { id: false });

	//------------------------------
	// {projects}

	var schema_projects = new Schema({
		id: { type: Number, unique: true },
		creator: { type: Schema.Types.ObjectId, ref: 'User' },
		name: String,
		description: String,
		date: Date,
		budget: Number,
		// 0: 待审批	1: 被拒绝	2: 已批准(选择学生)
		// 3: 开题	4: 中期	5: 结题	>5: 已完成
		state: { type: Number, default: 0 },
		// 0: empty	1: 审批	2: 拒绝	3: 通过
		report_state: { type: Number, default: 0 },
		applicants: [{ type: Schema.Types.ObjectId, ref: 'User' }],
		participants: [{ type: Schema.Types.ObjectId, ref: 'User' }]
	}, { id: false });

	schema_projects.path('date').get(e => e.toLocaleDateString('zh-Hans-CN'));
	schema_projects.set('toJSON', { getters: true, virtuals: false });

	// do NOT use arrow function for 'this' binding
	schema_projects.methods.add_applicants = function(students) {
		if (this.state != 2) throw 'invalid';
		students.forEach(e => this.applicants.indexOf(e) == -1
			&& this.applicants.push(e));
		return this;
	};

	schema_projects.methods.add_participants = function(students) {
		if (this.state != 2) throw 'invalid';
		students.forEach(e => this.participants.indexOf(e) == -1
			&& this.participants.push(e));
		return this;
	};

	//------------------------------
	// {activities}

	var schema_activities = new Schema({
		id: { type: Number, unique: true },
		creator: { type: Schema.Types.ObjectId, ref: 'User' },
		project: { type: Schema.Types.ObjectId, ref: 'Project' },
		content: String,
		cost: Number,
		date: Date
	}, { id: false });

	schema_activities.path('date').get(e => e.toLocaleDateString('zh-Hans-CN'));
	schema_activities.set('toJSON', { getters: true, virtuals: false });

	//------------------------------
	// {reports}

	var schema_reports = new Schema({
		id: { type: Number, unique: true },
		//creator: { type: Schema.Types.ObjectId, ref: 'User' },
		project: { type: Schema.Types.ObjectId, ref: 'Project' },
		content: String,
		// 1: 开题	2: 中期	3: 结题
		type: Number,
		// 1: 审批	2: 拒绝	3: 通过
		state: { type: Number, default: 1 },
		date: Date
	}, { id: false });

	schema_reports.path('date').get(e => e.toLocaleDateString('zh-Hans-CN'));
	schema_reports.set('toJSON', { getters: true, virtuals: false });

	//==============================

	//==============================
	// compile models

	var Counter = mongoose.model('Counter', schema_counters);
	// FIXME may be better
	Counter.next_seq = name => {
		return Counter.findOneAndUpdate(
			{ name },
			{ $inc: { seq: 1 } },
			{ new: true, upsert: true }
		).exec().then(x => x.seq);
	};

	var User = mongoose.model('User', schema_users);
	var Project = mongoose.model('Project', schema_projects);
	var Activity = mongoose.model('Activity', schema_activities);
	var Report = mongoose.model('Report', schema_reports);

	//==============================

	//==============================
	// API definition

	var _exclude = { _id: 0, __v: 0 };

	var list_query = (model, query, after, sort, limit) => {
		query = query || {};
		after = after || -1;
		sort = sort || -1;
		limit = limit || 10;
		if (after >= 0) {
			query.id = { $lt: after };
		}
		return model.find(query, _exclude).sort({ id: sort }).limit(limit);
	};

	var api = {
		user_login: (username, password) => {
			return User.findOne({
				username,
				credential: { $exists: true }
			}).exec().then(user => {
				if (!user) {
					throw 'not exist';
				}
				// FIXME check credential
				if (user.credential != password) {
					throw 'auth failed';
				}
				var token = utils.make_token(user.seq, user.role);
				return { token, name: user.name };
			});
		},
		user_revoke_self: (_self) => {
			'@roles'
			return Counter.next_seq('users_seq').then(seq => {
				return User.findByIdAndUpdate(_self._id, { $set: { seq } }).exec();
			});
		},
		user_validate: (username, password) => {
			//
		},
		user_password_self: (value, _self) => {
			'@roles'
			return Counter.next_seq('users_seq').then(seq => {
				return User.findByIdAndUpdate(_self._id, {
					$set: { seq, credential: value }
				}).exec();
			});
		},
		user_password: (user, value) => {
			'@roles admin'
			return Counter.next_seq('users_seq').then(seq => {
				return User.findOneAndUpdate({ id: user }, {
					$set: { seq, credential: value }
				}).exec();
			});
		},
		user_add: (username, password, role, name, telephone) => {
			'@roles admin'
			var user = new User({
				username,
				credential: password, // FIXME encryption
				role, name, telephone
			});
			return Counter.next_seq('users_id').then(id => {
				Counter.next_seq('users_seq').then(seq => {
					user.id = id;
					user.seq = seq;
					user.save();
					return user;
				})
			});
		},
		user_list: (after) => {
			'@roles admin'
			return list_query(User, {}, after).exec();
		},
		user_leave: (user) => {
			'@roles admin'
			// FIXME set credential to NULL?
			return User.findOneAndRemove({ id: user }).exec();
		},

		project_add: (name, description, budget, _self) => {
			'@roles teacher'
			var prj = new Project({
				name, description, budget,
				creator: _self._id,
				date: new Date()
			});
			Counter.next_seq('projects_id').then(id => {
				prj.id = id;
				prj.save();
				return prj;
			});
		},
		project_list: (_self, after) => {
			'@roles student oaa'
			switch (_self.role) {
				case ROLE.student:
					return list_query(Project, { state: 2 }, after)
						.populate('creator applicants participants')
						.find({ $nor: [
							{ applicants: { _id: _self._id } },
							{ participants: { _id: _self._id } }
						]})
						.exec();
				case ROLE.oaa:
					return list_query(Project, {}, after)
						.populate('creator')
						.exec();
				default:
					break;
			}
		},
		project_list_self: (_self, after) => {
			'@roles teacher'
			return list_query(Project, { creator: _self._id }, after)
				.select('-creator -applicants -participants')
				.exec();
		},
		project_approve: (project) => {
			'@roles oaa'
			return Project.findOneAndUpdate({ id: project }, {
				$set: { state: 2 }
			});
		},
		project_reject: (project, reason) => {
			'@roles oaa'
			return Project.findOneAndUpdate({ id: project }, {
				$set: { state: 1 }
			});
		},
		project_student_list: (project, after) => {
			'@roles teacher'
			// FIXME use after
			return Project.findOne({ id: project })
				.populate('applicants participants')
				.exec().then(p => {
					var field = p.state == 2 ? 'applicants'
						: p.state == 3 ? 'participants' : 'invalid';
					return p[field];
				});
		},
		project_student_approve: (project, $students) => {
			'@roles teacher'
			return Project.findOne({ id: project })
				.exec().then(prj => {
					return User.find({ id: { $in: $students } })
						.exec().then(s => {
							prj.add_participants(s.map(e => e._id));
							prj.state = 3;
							prj.save();
							return prj;
						});
				});
		},
		project_activity_add: (project, content, cost, _self) => {
			'@roles student'
			return Project.findOne({ id: project })
				.exec().then(prj => {
					var act = new Activity({
						creator: _self._id,
						project: prj._id,
						content, cost,
						date: new Date()
					});
					Counter.next_seq('activities_id').then(id => {
						act.id = id;
						act.save();
						return act;
					});
				});
		},
		project_activity_list: (project, after) => {
			'@roles teacher oaa'
			return list_query(Activity, {}, after)
				.populate('project creator')
				.exec().then(ls => ls.filter(e => e.project.id == project));
		},
		project_activity_list_self: (project, _self, after) => {
			'@roles student'
			return list_query(Activity, { creator: _self._id }, after)
				.populate('project')
				.exec().then(ls => ls.filter(e => e.project.id == project));
		},
		project_report_add: (project, content, _self) => {
			'@roles student'
			return Project.findOneAndUpdate({ id: project }, {report_state: 1})
				.exec().then(prj => {
					var rep = new Report({
						project: prj._id,
						content,
						type: prj.state - 2,
						date: new Date()
					});
					Counter.next_seq('reports_id').then(id => {
						rep.id = id;
						rep.save();
						return rep;
					});
				});
		},
		project_report_list_open: (project, after) => {
			'@roles teacher oaa student'
			return list_query(Report, {type : 1}, after)
				.populate('project')
				.exec().then(ls => ls.filter(e => e.project.id == project));
		},
		project_report_list_middle: (project, after) => {
			'@roles teacher oaa student'
			return list_query(Report, {type : 2}, after)
				.populate('project')
				.exec().then(ls => ls.filter(e => e.project.id == project));
		},
		project_report_list_final: (project, after) => {
			'@roles teacher oaa student'
			return list_query(Report, {type : 3}, after)
				.populate('project')
				.exec().then(ls => ls.filter(e => e.project.id == project));
		},
		project_report_approve: (report) => {
			'@roles teacher'
			return Report.findOneAndUpdate({ id: report }, { state: 3 })
					.populate('project')
					.exec().then(rep => {
						return Project.findByIdAndUpdate(rep.project._id, {report_state : 0, $inc: {state: 1} });
					});
		},
		project_report_reject: (report) => {
			'@roles teacher'
			return Report.findOneAndUpdate({ id: report }, { state: 2 })
					.populate('project')
					.exec().then(rep => {
						return Project.findByIdAndUpdate(rep.project._id, {report_state : 2});
					});
		},

		student_apply: (project, _self) => {
			'@roles student'
			return Project.findOne({ id: project })
				.exec().then(prj => prj.add_applicants([_self._id]).save());
		},
		student_cancel: (project, _self) => {
			'@roles student'
			return Project.findOneAndUpdate(
				{ id: project },
				{ $pull: { applicants: _self._id } }).exec();
		},
		student_list_project_pending: (_self, after) => {
			'@roles student'
			return list_query(Project, { state: 2 }, after)
				.populate('creator applicants')
				.find({ applicants: { _id: _self._id } })
				.select('-applicants -participants')
				.exec();
		},
		student_list_project: (_self, after) => {
			'@roles student'
			return list_query(Project, { state: {$gte : 3} }, after)
				.populate('creator participants')
				.find({ participants: { _id: _self._id } })
				.select('-applicants -participants')
				.exec();
		},
	};

	//==============================

	// set privileges
	var privileges = {};
	Object.keys(api).map(e => ({ key: e, value: api[e] })).forEach(e => {
		var roles = e.value.toString().match(/'@roles.*?'/);
		if (roles) {
			privileges[e.key] = [];
			roles[0].replace(/'/g, '').split(' ').slice(1)
				.forEach(x => privileges[e.key].push(ROLE[x]));
			if (privileges[e.key].length == 0) {
				privileges[e.key] = Object.keys(ROLE).map(e => ROLE[e]);
			}
		}
	});

	var check_privilege = (api, token) => {
		// privilege undefined <=> don't need token
		if (!privileges[api]) {
			return Promise.resolve(1);
		}
		if (!token) throw 'denied';
		var raw_token = utils.unpack_token(token);
		if (privileges[api].indexOf(raw_token.role) === -1) throw 'denied';
		return User.findOne({ seq: raw_token.seq }).exec().then(user => user);
	};

	var connect = (err, ok) => {
		err = err || (() => { });
		ok = ok || (() => { });
		mongoose.connect('mongodb://localhost/' + db_name);
		var db = mongoose.connection;
		db.on('error', err);
		db.once('open', ok);
	};

	return {
		connect, api, check_privilege
	}

})(require('mongoose'), 'rsms');

module.exports = DB;
